﻿<?php
class TwitterBot 
{
	public $db;
	public $twitter;
	public $config;
	private $responseFilters;
	public $lastStatusUpdate;
	public $lastUtterance;
	public $lastUtteranceTime;
	public $lastFollowTime;
	public $cursor;
	
	public function __construct($config)
	{	
		
		$this->config = $config;									// アクションと条件プラグインがコンフィグデータをアクセス出来るためのクラス変数
		$this->cursor = -1;											// 複数ページのフォローワーとフォローデータをナビゲートする変数
																	// 一ページをアクセス出来るため-1に設定しなければならない
		$this->log = fopen(LOG . $this->config['logfile'], 'a');	// BOTのログファイル
		
		$this->db = new mysqli($config['mysql_host'],$config['mysql_user'],$config['mysql_password'],$config['mysql_database']);
		$this->db->query("SET NAMES 'utf8'"); // こうしないとデータベースからのデータは文字化けになってしまう
		$this->twitter = new TwitterOAuth($config['consumer_key'], $config['consumer_secret'], $config['oauth_token'], $config['oauth_secret']);
		
		$this->logEvent("===========================================");
		$this->checkRateAPI();
		$this->loadSettings();
		$this->responseFilters = array();
		$this->chatFilters = array();
		$this->loadFilters();
		$this->logEvent("Initialized Bot.");
	}
	
	function __destruct()
	{
		$this->logEvent("===========================================");
		fclose($this->log);
	}
	
	function checkRateAPI()
	{
		$rate_limit = $this->twitter->get('account/rate_limit_status');
		//　API限界と近い
		if($rate_limit->remaining_hits < $this->config['acceptable_api_rate'])
		{
			$this->logEvent($this->config['api_limit_close'], 
				array('{{api_remaining}}', '{{api_total}}'), 
				array($rate_limit->remaining_hits, $rate_limit->hourly_limit)
			);
		}
		else
		{
			$this->logEvent($this->config['general_api_limit'], 
				array('{{api_remaining}}', '{{api_total}}', '{{reset_time}}'), 
				array($rate_limit->remaining_hits, $rate_limit->hourly_limit, date("Y-m-d h:i:s", strtotime($rate_limit->reset_time)))
			);
		}
	}
	
	public function logEvent($message, $searches = '', $replacements = '')
	{
		if($searches && $replacements && is_array($searches) && is_array($replacements) && count($searches) == count($replacements))
		{
			$message = str_replace($searches, $replacements, $message);
		}
	
		$log = 
		$message = "[" . date("Y-m-d h:i:s") . "] $message\n";
		fwrite($this->log, $message);
	}
	
	private function loadSettings()
	{
		$sql = "SELECT name, value FROM setting";
		$settings = $this->db->query($sql);
		
		if($settings)
		{
			while($setting = $settings->fetch_assoc())
			{
				// $this->{$setting['name']}はクラス変数を動的な名前でアクセスする方法
				$this->{$setting['name']} = $setting['value'];
			}
		}
		
		$this->logEvent("設定ロード終了");
		
		$settings->close();
	}
	
	private function loadFilters()
	{
		foreach (new DirectoryIterator('actions/enabled/') as $action) {
    		if($action->isDot() || basename($action->getFilename()) == '.svn') continue;
    		require_once(ACTIONS . '/' . $action->getFilename());
		}

		foreach (new DirectoryIterator('conditions/enabled/') as $condition) {
    		if($condition->isDot() || basename($condition->getFilename()) == '.svn') continue;
    		require_once(CONDITIONS . '/' . $condition->getFilename());
		}
		
		/**
		 * フィルターシステムは条件を満たせば、アクションを実行するシステムです。フィルターは「filter」テーブルで存在している。
		 * フィルターと条件は「filter_conditions」で繋がっている。フィルターとアクションは「filter_actions」で繋がっている。
		 */
		
		$sql = "
		SELECT action.name, action.value, filter.name as filter_name, filter.type as filter_type
		FROM action
		INNER JOIN filter_action ON action.id = filter_action.action_id
		INNER JOIN filter ON filter_action.filter_id = filter.id
		WHERE filter.enabled = 1
		AND filter_action.enabled = 1
		ORDER BY filter.display_order,filter_action.display_order ASC
		";
		
		$actions = $this->db->query($sql);
		
		while($action = $actions->fetch_assoc())
		{
			// フィルターのタイプ。現在タイプは「chat」(ランダム発言）と「response」（ユーザーと会話する）
			$this->{$action['filter_type'] . 'Filters'}[$action['filter_name']]['actions'][] = $action;
		}
		
		$sql = "
		SELECT `condition`.name, `condition`.value, filter.name as filter_name, filter.type as filter_type
		FROM `condition`
		INNER JOIN filter_condition ON `condition`.id = filter_condition.condition_id
		INNER JOIN filter ON filter_condition.filter_id = filter.id
		WHERE filter.enabled = 1
		AND filter_condition.enabled = 1
		";
		
		$conditions = $this->db->query($sql);
		
		while($condition = $conditions->fetch_assoc())
		{
			$this->{$condition['filter_type'] . 'Filters'}[$condition['filter_name']]['conditions'][] = $condition;
		}
		
		$this->logEvent("フィルターロード終了");
		
		$conditions->close();
		$actions->close();
	}
	
	public function follow()
	{
		//if(false)
		if(time() <= strtotime($this->config['follow_delay'], $this->lastFollowTime) && $this->lastFollowTime)
		{
			return false;
		}
		else
		{
			$this->lastFollowTime = time();
		}
		
		$needsFollowing = array();
		$needsRemoving = array();
		$following = array();
		$followed = array();
		
		$following = $this->gatherFollow('friends');
		// APIのページカーサーを元にリセット
		$this->cursor = -1;
		$followed = $this->gatherFollow('followers');
		$this->cursor = -1;
		
		if(empty($following) || empty($followed))
		{
			$this->logEvent($this->config['no_follows_or_followers']);
			return false;
		}
		
		/*// TwitterAPIドキュメンテーションにより自動リムーブは禁止らしいのでとりあえずコメントしました。
		$needsRemoving = $this->array_diff_nlevel($following,$followed);
		if(count($needsRemoving) > 0)
		{
			//　リムーブしすぎかも～
			if(count($needsRemoving) > $this->config['acceptable_remove_rate'])
			{
				$this->twitter->post('statuses/update', array('status' => $this->config['remove_warning']));
				$this->logEvent(
					$this->config['too_many_removes'], 
					array('{{number_of_removes}}'), 
					array(count($needsRemoving))
				);
				return false;
			}
			
			foreach($needsRemoving as $id => $user_data)
			{
				$this->twitter->post('friendships/destroy', array('user_id' => $id));
			}
			
			$this->logEvent($this->config['removes_made'], array('{{remove_count}}'), array(count($needsRemoving)));
			return true;
		}*/

		$needsFollowing = $this->array_diff_nlevel($followed, $following);
		if(count($needsFollowing))
		{
			// フォローしすぎかも～
			if(count($needsFollowing) > $this->config['acceptable_follow_rate'])
			{
				$this->twitter->post('statuses/update', array('status' => $this->config['follow_warning']));
				$this->logEvent(
					$this->config['too_many_follows'], 
					array('{{number_of_follows}}'), 
					array(count($needsFollowing))
				);
				return false;
			}
			foreach($needsFollowing as $id => $user_data)
			{
				// 非公開ユーザーは一度フォローして、それからAPIを使いすぎないため
				// そのユーザーのIDをデータベースに入れて、一度ユーザーにリクエストの確認を頼む
				if($user_data['protected'])
				{
					$sql = "
					SELECT user_id
					FROM private_follow
					WHERE user_id = $id
					";
					
					$result = $this->db->query($sql);
					if($result->num_rows == 0)
					{
						$this->twitter->post('friendships/create', array('user_id' => $id, 'follow' => true));
						$message = str_replace(array('{{username}}'), array($user_data['screen_name']), $this->config['new_follow_locked']);
						$this->twitter->post('statuses/update', array('status' => $message));

						$sql = "INSERT INTO private_follow (user_id) VALUES($id)";
						$this->db->query($sql);
					}
				}
				else
				{
					$this->twitter->post('friendships/create', array('user_id' => $id, 'follow' => true));
					// フォロー挨拶
					$message = str_replace(
						array('{{username}}', '{{displayname}}'), 
						array($user_data['screen_name'], $user_data['name']), 
						$this->config['new_follow']
					);
					$this->twitter->post('statuses/update', array('status' => $message)); 
				}
			}
			
			$this->logEvent($this->config['follows_made'], array('{{follow_count}}'), array(count($needsFollowing)));
		}
	}
	
	private function array_diff_nlevel($from, $against)
	{
		$return = array();
	
		foreach($from as $key => $value)
		{
			if(!isset($against[$key]))
			{
				$return[$key] = $value;
			}
		}
		
		return $return;
	}
	
	private function gatherFollow($type, $follow_result = array())
	{
		$follows = $this->twitter->get("statuses/$type", array('cursor' => $this->cursor));
		
		foreach($follows->users as $follow)
		{
			if(property_exists($follow, 'id'))
			{
				$follow_result["{$follow->id}"] = array(
				'name' => $follow->name, 
				'screen_name' => $follow->screen_name, 
				'protected' => $follow->{'protected'}
				);
			}
		}
		
		// フォロー・フォローワーページがまだあります。次のカーソルポイントを見付かる。
		if(property_exists($follows, 'next_cursor_str') && $follows->next_cursor_str != '0')
		{
			$this->cursor = $follows->next_cursor_str;
			$follow_result = $this->gatherFollow($type, $follow_result);
		}
		
		return $follow_result;
	}
	
	public function createDialogue()
	{
		// 発言の遅延を確認する
		if(time() <= strtotime($this->config['dialogue_delay'], $this->lastUtteranceTime) && $this->lastUtteranceTime)
		{
			return false;
		}

		// BOTがもう発言したかどうか
		$response_handled = false;
	
		foreach($this->chatFilters as $filter_name => $filter)
		{
			// 発言はもうしたので、ここでループは終了
			if($response_handled)
				break;
		
			$filter_condition = true;
			
			foreach($filter['conditions'] as $condition)
			{
				// 条件クラスが存在してない
				if(!class_exists($condition['name']))
				{
					$this->logEvent("[エーラー！]条件クラス{$condition['name']}は存在していません。");
					$filter_condition = false;
					break;
				}
					
				// 全ての条件クラスは「check」と言う静的関数を実行する
				if(call_user_func($condition['name'] . '::check', '', $condition['value']))
				{
					continue;
				}
				else
				{
					// 一つの条件が失敗したら実行する必要はない
					$filter_condition = false;
					break;
				}
			}
			
			if($filter_condition == true)
			{
				$response_handled = true;
				
				foreach($filter['actions'] as $action)
				{
					if(!class_exists($action['name']))
					{
						$this->logEvent("[エーラー！]アクションクラス{$action['name']}は存在していません。");
						return;
					}
					// 全てのアクションクラスは「execute」と言う静的関数を実行する
					call_user_func($action['name'] . '::execute', $this, '', $action['value']);
					$this->lastUtteranceTime = time();
				}
			}
		}
	}
	
	public function parseTimeline()
	{
		$timeline = $this->twitter->get('statuses/friends_timeline', array('count' => 200));
		
		// ユーザーのツイートを読み込んだかどうかを分かるためデータベースにツイート情報を保存する
		if(is_array($timeline))
		{
			foreach($timeline as $tweet)
			{
				$this->saveTweet($tweet);
			}
		}
		
		$this->cleanupTweets();
		$this->processTweets();
		$this->markTweetsRead();
	}
	
	protected function processTweets()
	{
		$sql = "
		SELECT id, username, display_name, message, date_created, user_id, reply_screen_name, reply_tweet_id
		FROM tweet
		WHERE `read` IS NULL
		";
		
		$tweets = $this->db->query($sql);
		
		$this->logEvent($tweets->num_rows . "ツイートを見つけました。");
		
		while($tweet = $tweets->fetch_assoc())
		{
			$response_handled = false;
		
			foreach($this->responseFilters as $filter_name => $filter)
			{
				if($response_handled)
					break;
			
				$filter_condition = true;
				
				foreach($filter['conditions'] as $condition)
				{
					if(!class_exists($condition['name']))
					{
						$this->logEvent("[エーラー！]条件クラス{$condition['name']}は存在していません。");
						$filter_condition = false;
						break;
					}
						
					if(call_user_func($condition['name'] . '::check', $this, $tweet, $condition['value']))
					{
						continue;
					}
					else
					{
						$filter_condition = false;
						break;
					}
				}
				
				if($filter_condition == true)
				{
					$response_handled = true;
					
					foreach($filter['actions'] as $action)
					{
						if(!class_exists($action['name']))
						{
							$this->logEvent("[エーラー！]アクションクラス{$action['name']}は存在していません。");
							return;
						}
						
						call_user_func($action['name'] . '::execute', $this, $tweet, $action['value']);
					}
				}
			}
		}
		
		$tweets->close();
	}
	
	private function saveTweet($tweet)
	{					
		$tweet->in_reply_to_status_id = ($tweet->in_reply_to_status_id) ? $tweet->in_reply_to_status_id : 'NULL';

		$sql = "INSERT INTO tweet (id, username, display_name, message, date_created, user_id, reply_screen_name, reply_tweet_id)
		VALUES(
		{$tweet->id},
		'" . $this->db->real_escape_string($tweet->user->screen_name) . "',
		'" . $this->db->real_escape_string($tweet->user->name) . "',
		'" . $this->db->real_escape_string($tweet->text) . "',
		" . strtotime($tweet->created_at) . ",
		{$tweet->user->id},
		'" . $this->db->real_escape_string($tweet->in_reply_to_screen_name) . "',
		{$tweet->in_reply_to_status_id}) 
		ON DUPLICATE KEY UPDATE id={$tweet->id}";
		
		$this->db->query($sql);
	}

	protected function markTweetsRead()
	{
		$sql = "UPDATE tweet SET `read` = 1";
		$this->db->query($sql);
		
		if($this->db->error)
		{
			echo $this->db->error . "\n";
		}		
	}	

	public function cleanupTweets()
	{
		// もう読み込んだ古いツイートを削除する
		$sql = "SELECT id FROM tweet WHERE `read` = 1 ORDER BY tweet.id DESC LIMIT 200, 1";
		$tweet = $this->db->query($sql);
		
		if($tweet->num_rows == 1)
		{
			$data = $tweet->fetch_assoc();
			
			$sql = "
			DELETE FROM tweet
			WHERE id <= {$data['id']}";
			
			$this->db->query($sql);
		}
	}
	
	public function saveSettings()
	{
		$settings = array();

		if($this->lastStatusUpdate)
			$settings['lastStatusUpdate'] = $this->lastStatusUpdate;
		if($this->lastUtterance)
			$settings['lastUtterance'] = $this->lastUtterance;
		if($this->lastUtteranceTime)
			$settings['lastUtteranceTime'] = $this->lastUtteranceTime;
		if($this->lastFollowTime)
			$settings['lastFollowTime'] = $this->lastFollowTime;	
		if(isset($this->lastDiagnosis))
			$settings['lastDiagnosis'] = $this->lastDiagnosis;	
			
		if($settings)
		{
			foreach($settings as $settingName => $settingValue)
			{
				$sql = "
				INSERT INTO setting (name, value) VALUES('$settingName', '" . $this->db->real_escape_string($settingValue) . "')
				ON DUPLICATE KEY UPDATE value = '" . $this->db->real_escape_string($settingValue) . "'";
				
				$this->db->query($sql);
			}
		}
	}
}
?>
